﻿using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Alderamin.Core
{
    /// <summary>
    /// Handles the HTTP and HTTPS requests
    /// </summary>
    public class AlderaminClientProxy
    {
        #region Private Fields
        /// <summary>
        /// The receiving buffer size
        /// </summary>
        private const int DEFAULT_BUFFER_SIZE = 8192;
        /// <summary>
        /// The ip address of localhost
        /// </summary>
        private IPAddress LOCALHOST_IP = IPAddress.Parse("127.0.0.1");
        /// <summary>
        /// The tcp listener
        /// </summary>
        private TcpListener _localListener;
        /// <summary>
        /// The https request handler
        /// </summary>
        private HttpsHandler _httpsHandler;
        /// <summary>
        /// The http requests handler
        /// </summary>
        private HttpHandler _httpHandler;
        /// <summary>
        /// The separate thread for receiving requests from browser
        /// </summary>
        private BackgroundWorker _proxyWorker;
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets a value indicates that whether the proxy has started
        /// </summary>
        public bool IsStarted { get; private set; }

        /// <summary>
        /// The configuration of the proxy.The changes of the configuration will not take affect
        /// until the restart of the proxy
        /// </summary>
        public ClientProxyConfiguration Configuration { get; set; }
        #endregion

        #region Construtor
        /// <summary>
        /// Initializes a new instance of the AlderaminClientProxy class with the specified configuration
        /// </summary>
        /// <param name="config">The proxy configuration</param>
        public AlderaminClientProxy(ClientProxyConfiguration config)
        {
            _proxyWorker = new BackgroundWorker();
            _proxyWorker.WorkerSupportsCancellation = true;
            _proxyWorker.DoWork += new DoWorkEventHandler(ProcessClientRequest);
            _httpHandler = new HttpHandler(config.HttpUrl);
            _httpsHandler = new HttpsHandler(config.AccessKey, config.HttpsStartUrl, config.HttpsMainUrl, config.HttpsRunningUrl);

            this.Configuration = config;
            this.IsStarted = false;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Start running the proxy in the background
        /// </summary>
        /// <returns>True if the proxy started successfully;otherwise, false</returns>
        public bool Start()
        {
            if (this.IsStarted)
                return false;

            // Create the tcp listener
            if (!CreateListener(this.Configuration.LocalListenPort))
                return false;

            // Reassign the variables of HttpHandler and HttpsHandler
            if (!ResetConfiguration())
                return false;

            if (_httpsHandler.Start())
            {
                _localListener.Start();
                if (this.Configuration.LocalListenPort == 0)
                {
                    this.Configuration.LocalListenPort = (_localListener.LocalEndpoint as IPEndPoint).Port;
                }
                _proxyWorker.RunWorkerAsync();
                this.IsStarted = true;
                return true;
            }
            else
            {
                _localListener = null;
                return false;
            }
        }

        /// <summary>
        /// Stop the proxy
        /// </summary>
        public void Stop()
        {
            if (this.IsStarted)
            {
                _proxyWorker.CancelAsync();
                try
                {
                    _localListener.Stop();
                }
                catch (SocketException) { }
                _httpsHandler.Stop();
                _localListener = null;
                this.IsStarted = false;
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Re-apply the changes of the configuration
        /// </summary>
        /// <returns>True if all the changes are applied successfully;otherwise, false</returns>
        private bool ResetConfiguration()
        {
            try
            {
                _httpHandler.MainScriptUrl = this.Configuration.HttpUrl;
                _httpHandler.Proxy = this.Configuration.Proxy;

                _httpsHandler.AccessKey = this.Configuration.AccessKey;
                _httpsHandler.MaxRunnings = this.Configuration.HttpsMaxRunnings;
                _httpsHandler.StartScriptUrl = this.Configuration.HttpsStartUrl;
                _httpsHandler.MainScriptUrl = this.Configuration.HttpsMainUrl;
                _httpsHandler.RunningScriptUrl = this.Configuration.HttpsRunningUrl;
                _httpsHandler.Proxy = this.Configuration.Proxy;

                return true;
            }
            catch (ArgumentOutOfRangeException)
            {
                return false;
            }
        }

        /// <summary>
        /// Process the requests from the client(e.g. browser)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void ProcessClientRequest(object sender, DoWorkEventArgs args)
        {
            while (!_proxyWorker.CancellationPending)
            {
                TcpClient client;
                try
                {
                    // Perform a blocking call to accept requests.
                    client = _localListener.AcceptTcpClient();
                }
                catch (SocketException)
                {
                    break;
                }

                BackgroundWorker requestWorker = new BackgroundWorker();
                requestWorker.DoWork += (s, a) =>
                {
                    // Buffer for reading data from the client
                    string requestString = String.Empty;
                    Byte[] buffer = new Byte[DEFAULT_BUFFER_SIZE];
                    NetworkStream clientStream = client.GetStream();
                    ASCIIEncoding asciiEncoding = new ASCIIEncoding();
                    try
                    {
                        // Loop to receive all the data sent by the client.
                        do
                        {
                            int bytesRead = clientStream.Read(buffer, 0, DEFAULT_BUFFER_SIZE);
                            // Encode data to a ASCII string.
                            requestString += asciiEncoding.GetString(buffer, 0, bytesRead);
                        }
                        while (clientStream.DataAvailable);

                        if (!String.IsNullOrEmpty(requestString))
                        {
                            RequestInfo requestInfo = RequestInfo.TryParse(requestString);
                            if (requestInfo != null)
                            {
                                if (requestInfo.Method == RequestMethod.Connect)
                                {
                                    _httpsHandler.AddStream(clientStream, requestInfo.RemoteHost + ":" + requestInfo.RemotePort);
                                }
                                else
                                {
                                    requestString = RewriteRequestHeader(requestString, requestInfo);
                                    _httpHandler.SendRequest(asciiEncoding.GetBytes(requestString), clientStream, requestInfo.RemoteHost + ":" + requestInfo.RemotePort);
                                }
                            }
                        }
                    }
                    catch (IOException)
                    {
                    }
                };
                requestWorker.RunWorkerAsync();
            }
            this.IsStarted = false;
        }

        /// <summary>
        /// Try to create the tcp listener with the specified port number
        /// </summary>
        /// <param name="port">The local port number to listen.If the port number is 0, an
        /// available port number between 1024 and 65536 will be assigned
        /// with the range</param>
        /// <returns>True if the listener is created successfully;otherwise,false</returns>
        private bool CreateListener(int port)
        {
            try
            {
                _localListener = new TcpListener(LOCALHOST_IP, port);
                return true;
            }
            catch (ArgumentOutOfRangeException)
            {
                return false;
            }
        }

        /// <summary>
        /// Modifies the request string
        /// </summary>
        /// <param name="requestString">The request string to be modified</param>
        /// <param name="info">The infomation of the request</param>
        /// <returns>An modified request string</returns>
        private string RewriteRequestHeader(string requestString, RequestInfo info)
        {
            StringBuilder rewriter = new StringBuilder(requestString);
            // Changed the absolute uri to a relative uri
            string relativeUri = info.RequestUri;
            if (relativeUri.Contains("http://"))
            {
                relativeUri = relativeUri.Replace("http://" + info.RemoteHost, "");
            }
            else if (relativeUri.Contains("https://"))
            {
                relativeUri = relativeUri.Replace("https://" + info.RemoteHost, "");
            }
            else if (relativeUri.Contains("ftp://"))
            {
                relativeUri = relativeUri.Replace("ftp://" + info.RemoteHost, "");
            }
            rewriter.Replace(info.RequestUri, relativeUri, 0, requestString.IndexOf("\r\n"));
            rewriter.Replace("Proxy-Connection: keep-alive", "Connection: close");

            return rewriter.ToString();
        }
        #endregion
    }
}
